package com.kamistoat.meimeimall.meimeimallorder.web;

import com.alibaba.fastjson.JSON;
import com.alipay.api.AlipayApiException;
import com.kamistoat.meimeimall.common.To.EsTo.OrderListTo;
import com.kamistoat.meimeimall.common.To.memberTo.LoginSuccessTo;
import com.kamistoat.meimeimall.common.To.paymentTo.PayTo;
import com.kamistoat.meimeimall.common.annotation.DubboAndHTTPExceptionCatch;
import com.kamistoat.meimeimall.common.annotation.LoginRequired;
import com.kamistoat.meimeimall.common.constant.AuthConstant;
import com.kamistoat.meimeimall.common.constant.OrderConstant;
import com.kamistoat.meimeimall.common.dubboInterface.ThirdPartyDubboInterface;
import com.kamistoat.meimeimall.common.exception.BizCodeEnum;
import com.kamistoat.meimeimall.common.exception.RRException;
import com.kamistoat.meimeimall.common.threadlocal.ThreadLocalContextHolder;
import com.kamistoat.meimeimall.common.utils.R;
import com.kamistoat.meimeimall.meimeimallorder.listener.OrderPayCallbackListener;
import com.kamistoat.meimeimall.meimeimallorder.vo.*;
import com.kamistoat.meimeimall.meimeimallorder.config.AlipayTemplate;
import com.kamistoat.meimeimall.meimeimallorder.entity.OrderEntity;
import com.kamistoat.meimeimall.meimeimallorder.feign.ThirdPartyFeignService;
import com.kamistoat.meimeimall.meimeimallorder.service.OrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.Reference;
import org.redisson.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.concurrent.TimeUnit;

@Api(tags = "订单流转页操作")
@RestController
@RequestMapping("web")
public class OrderTransPageController {
    protected Logger logger = LoggerFactory.getLogger(OrderTransPageController.class);

    @Autowired
    OrderService orderService;
    @Autowired
    AlipayTemplate alipayTemplate;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    ThirdPartyFeignService thirdPartyFeignService;
    @Autowired
    OrderPayCallbackListener orderPayCallbackListener;

    @Reference
    ThirdPartyDubboInterface thirdPartyDubboInterface;

    /**
     * 在订单检查页面，点击支付按钮.
     * 订单中可以同时含有普通商品和促销商品
     *
     * @return R对象
     */
    @ApiOperation("在订单检查页点击去支付")
    @PostMapping("/submit_order")
    @DubboAndHTTPExceptionCatch
    @LoginRequired
    public R submitOrder(OrderSubmitVo orderSubmitVo) {
        // 非空检查。防止有人直接用网址路由来发请求
        if (orderSubmitVo == null) {
            throw new RRException("不允许通过外链直接访问该页面", BizCodeEnum.BACK_TO_CART.getCode());
        }
        logger.info("接受{}订单下单请求");
        /**
         * 服务层执行全局事务并提交消息队列。如果此处正常执行完，说明下单消息已经提交成功，则通知前端开启webSocket检查结果
         */
        orderService.submitOrderForAllType(orderSubmitVo);
        // 未抛出异常，则通知前端监听. 抛出的异常被注解处理
        return R.ok("下单消息提交成功，前端开始WebSocket监听");
    }


    /**
     * 对处于等待支付状态的订单，点击 去支付 跳转到对应该订单的支付页面。
     * 同样使用限流器，不限制多个登录实例，但是限制冲击数据库的频率，同时锁定使用错误订单SN码故意冲击接口的行为
     *
     * @param orderSN 订单SN
     */
    @ApiOperation("待支付订单返回支付")
    @GetMapping("/return_to_pay")
    @DubboAndHTTPExceptionCatch
    @LoginRequired
    public R returnPay(@RequestParam String orderSN) {
        LoginSuccessTo loginSuccessTo = JSON.parseObject((String) ThreadLocalContextHolder.get(AuthConstant.LOGIN_USER_THREADLOCAL_NAME), LoginSuccessTo.class);

        /**
         * 检查账户是否已经被锁定支付功能。拦截使用错误订单SN码故意冲击接口的行为
         */
        RLock banLock = redissonClient.getLock(OrderConstant.ORDER_ABNORMAL_LOCK_PREFIX + loginSuccessTo.getId());
        if (banLock.isLocked()) {
            throw new RRException(BizCodeEnum.PAY_FUNC_LOCKED.getMessage(), BizCodeEnum.PAY_FUNC_LOCKED.getCode());
        } else {
            /**
             * 没有锁定，但是也不允许高强度冲击数据库
             * 获取redisson限流器，每次访问都延长其过期时间为5分钟后
             */
            RRateLimiter payWaitCheckLimiter = redissonClient.getRateLimiter("order:payWaitCheck:rateLimit:" + loginSuccessTo.getId());
            stringRedisTemplate.expire("order:payWaitCheck:rateLimit:" + loginSuccessTo.getId(), 5, TimeUnit.MINUTES);
            // 限流器初始化设置为每1秒提供2个令牌。如果限流器已存在，则该命令失效
            payWaitCheckLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
            // 获取限流器令牌，超时时间是2秒。因此只要当前只有一个账号实例，必然能够获取到，因为前置
            boolean payWaitCheckAcquire = payWaitCheckLimiter.tryAcquire(1, 2, TimeUnit.SECONDS);

            if (payWaitCheckAcquire) {
                // 带缓查询订单
                OrderEntity orderEntity = orderService.getUnpayOrder(loginSuccessTo.getId(), orderSN);
                // 有可能出现有人不停使用错误的orderSN直连接口。因此需要设置一个异常锁
                if (orderEntity != null) {
                    return R.ok().put("orderEntity", orderEntity);
                } else {
                    /**
                     * 说明用户直接使用无效订单SN+外部链接访问该接口，则统计次数，如果在2分钟内出现5次异常支付，将会锁定该账号
                     */
                    RRateLimiter abnormalPayLimiter = redissonClient.getRateLimiter(OrderConstant.ORDER_ABNORMAL_RATE_PREFIX + loginSuccessTo.getId());
                    stringRedisTemplate.expire("order:abnormalPay:rateLimit:" + loginSuccessTo.getId(), 5, TimeUnit.MINUTES);  // 每次延长过期时间5分钟
                    abnormalPayLimiter.trySetRate(RateType.OVERALL, 5, 2, RateIntervalUnit.MINUTES);
                    boolean abnormalPayAcquire = abnormalPayLimiter.tryAcquire();

                    // 如果已经触发账户封禁
                    if (!abnormalPayAcquire) {
                        // 设置自动过期锁，不手动解锁
                        banLock.lock(3, TimeUnit.MINUTES);
                    }
                    throw new RRException(BizCodeEnum.PAY_UNPAY_ERROR.getMessage(), BizCodeEnum.PAY_UNPAY_ERROR.getCode());
                }
            } else {
                throw new RRException(BizCodeEnum.BLOCKED_BY_SENTINEL.getMessage(), BizCodeEnum.BLOCKED_BY_SENTINEL.getCode());
            }
        }
    }


    /**
     * 在支付页面点击支付，调用该接口。该接口会进行订单检查，订单未超时且处于未支付状态则拉起第三方支付工具进行支付。
     * <p>
     * 注意，这里涉及到：普通订单的过期时间总是固定，但是秒杀活动的订单则不同，秒杀商品的结束时间是固定的，因此不同时间的订单的过期时间是不固定的
     * 到时候写订单系统的时候，关单系统记得要匹配一下
     * <p>
     * 幂等逻辑限制：相同的订单不允许迸发支付。锁的持有应该贯穿该接口的整个生命周期。
     *
     * @param orderSn 订单sn
     * @return R对象。内部含有支付宝生成的支付页面 Html 代码
     * @throws AlipayApiException
     */
    @ApiOperation("拉起第三方支付")
    @GetMapping(value = "/pay_order")
    @DubboAndHTTPExceptionCatch
    @LoginRequired
    public R payOrder(@RequestParam String orderSn) throws AlipayApiException {
        RLock orderStatusModifyLock = redissonClient.getLock(OrderConstant.ORDER_MODIFY_LOCK_PREFIX + orderSn);
        // 如果抢不到该订单的修改锁，则说明该订单正处于修改状态。可能是正在被其他线程支付，或者被其他动作修改
        if (!orderStatusModifyLock.tryLock()) {
            throw new RRException(BizCodeEnum.ORDER_MODIFY_LOCKED.getMessage(), BizCodeEnum.ORDER_MODIFY_LOCKED.getCode());
        } else {
            try {
                LoginSuccessTo loginSuccessTo = JSON.parseObject((String) ThreadLocalContextHolder.get(AuthConstant.LOGIN_USER_THREADLOCAL_NAME), LoginSuccessTo.class);

                // 根据订单构建支付Vo类
                PayTo payTo = orderService.getOrderPayVo(loginSuccessTo.getId(), orderSn);

                if (payTo == null) {
                    throw new RRException(BizCodeEnum.PAY_UNPAY_ERROR.getMessage(), BizCodeEnum.PAY_UNPAY_ERROR.getCode());
                } else {
                    // 支付宝pay方法返回的是一段html代码，直接嵌入在R对象中通知前端执行即可
                    String payHTML = alipayTemplate.pay(payTo);
                    return R.ok().put("payHTML", payHTML);
                }
            } finally {
                orderStatusModifyLock.unlock();
            }
        }
    }


    /**
     * 绕过真实支付，直接向监听接口发送支付成功回调. 用于调试阶段
     *
     * @return
     */
    @ApiOperation("拉起虚拟支付。绕过真实支付")
    @GetMapping(value = "/pay_order_unreal")
    @LoginRequired
    @DubboAndHTTPExceptionCatch
    public R payOrderUnreal(@RequestParam String orderSn) throws InterruptedException {
        RLock orderStatusModifyLock = redissonClient.getLock(OrderConstant.ORDER_MODIFY_LOCK_PREFIX + orderSn);
        // 如果抢不到该订单的修改锁，则说明该订单正处于修改状态。可能是正在被其他线程支付，或者被其他动作修改
        if (!orderStatusModifyLock.tryLock()) {
            throw new RRException(BizCodeEnum.ORDER_MODIFY_LOCKED.getMessage(), BizCodeEnum.ORDER_MODIFY_LOCKED.getCode());
        } else {
            try {
                LoginSuccessTo loginSuccessTo = JSON.parseObject((String) ThreadLocalContextHolder.get(AuthConstant.LOGIN_USER_THREADLOCAL_NAME), LoginSuccessTo.class);
                // 根据订单构建支付Vo类
                PayTo payTo = orderService.getOrderPayVo(loginSuccessTo.getId(), orderSn);
                if (payTo == null) {
                    throw new RRException(BizCodeEnum.PAY_UNPAY_ERROR.getMessage(), BizCodeEnum.PAY_UNPAY_ERROR.getCode());
                } else {
                    // 绕过真实支付，睡眠2秒模拟支付，然后直接向成功支付发送回调
                    Thread.sleep(2000);
                    return orderPayCallbackListener.handlePayedUnreal(orderSn, payTo.getTotal_amount());
                }
            } finally {
                orderStatusModifyLock.unlock();
            }
        }
    }
}








